/*-

 FlippyFloppyView.m

 Warp, twist, and bounce a piece of text around the screen.

 Copyright (c) 1992 by Scott Byer

 Permission is given to freely distribute and modify this code
 provided the copyright notice is left unchanged.
 No warranties, explicit or implied, apply to this code.
*/

#import "FlippyFloppyView.h"

#import <appkit/appkit.h>

#import <dpsclient/dpsclient.h>
#import <dpsclient/psops.h>

#import <libc.h>
#import <stdlib.h>

#include "FlippyFloppyWraps.h"

#define	XMOVEFACTOR	(0.1 * hyperness)
#define YMOVEFACTOR	(0.1 * hyperness)

#define XSPEEDMAX	(20.0 * hyperness)
#define YSPEEDMAX	(20.0 * hyperness)
#define XSPEEDMIN	(-20.0 * hyperness)
#define YSPEEDMIN	(-20.0 * hyperness)

#define ROTATEFACTOR	(0.5 * hyperness)

#define ROTATESPEEDMAX	(20.0 * hyperness)
#define ROTATESPEEDMIN	(-20.0 * hyperness)

#define XSCALEFACTOR	(0.05 * hyperness)
#define YSCALEFACTOR	(0.05 * hyperness)

#define XSCALESPEEDMAX	(0.1 * hyperness)
#define YSCALESPEEDMAX	(0.1 * hyperness)
#define XSCALESPEEDMIN	(-0.1 * hyperness)
#define YSCALESPEEDMIN	(-0.1 * hyperness)

#define XSCALEMIN	-5.0
#define YSCALEMIN	-5.0
#define XSCALEMAX	5.0
#define YSCALEMAX	5.0

#define SCALENOMANSLAND	0.08

#define COLORFACTOR	(0.01 * hyperness)

#define COLORSPEEDMAX	(0.05 * hyperness)
#define COLORSPEEDMIN	(-0.05 * hyperness)

#define COLORMIN	0.1
#define COLORMAX	1.0

@implementation FlippyFloppyView

#define	DRANDOM(factor)	((random() / 2147483647.0) * (factor) - ((factor) / 2.0))

- oneStep
  {
    int	whichLine;

    /* Erase the old text by doing a transform and drawing a black
       rectangle.							   */
    FFWTransform(x, y, xscale, yscale, rotation);
    FFWBlackBox(stringsBBox.llx, stringsBBox.lly,
                  stringsBBox.urx, stringsBBox.ury);
    FFWDeTransform(-x, -y, 1.0/xscale, 1.0/yscale, -rotation);

    /* If the font or the text to display has changed, go figure out what
       is now different, and act accordingly. */
    if (changed) {
      [self somethingChanged];
      changed = NO;
      }
      
    /* Sanity check to make sure we have at least one string to display. */
    if (numLines == 0) return self;
     
    /* Change the movemoent of the characters				   */
    xspeed += DRANDOM(XMOVEFACTOR);
    yspeed += DRANDOM(YMOVEFACTOR);

    /* Bounds checking for the movement speed.				   */
    if (xspeed < XSPEEDMIN) xspeed = XSPEEDMIN;
    if (xspeed > XSPEEDMAX) xspeed = XSPEEDMAX;

    if (yspeed < YSPEEDMIN) yspeed = YSPEEDMIN;
    if (yspeed > YSPEEDMAX) yspeed = YSPEEDMAX;

    /* Change to location of the characters based on the new movement.	   */
    x += xspeed;
    y += yspeed;

    /* Bounds checking for the absolute location.			   */
    if (x < 0.0) {
	x = 0.0;
	xspeed = -xspeed;
      }
    if (x > xmax) {
	x = xmax;
	xspeed = -xspeed;
      }

    if (y < 0.0) {
	y = 0.0;
	yspeed = -yspeed;
      }
    if (y > ymax) {
	y = ymax;
	yspeed = -yspeed;
      }

    /* Change the rate of rotation.					   */
    rotationspeed += DRANDOM(ROTATEFACTOR);

    /* Bounds check the rotation rate.					   */
    if (rotationspeed < ROTATESPEEDMIN) rotationspeed = ROTATESPEEDMIN;
    if (rotationspeed > ROTATESPEEDMAX) rotationspeed = ROTATESPEEDMAX;

    /* Add the rotation in.						   */
    rotation += rotationspeed;

    /* Bounds check the rotation angle.					   */
    if (rotation < 0.0) rotation += 360.0;
    if (rotation > 360.0) rotation -= 360.0;

    /* Change the rate of scaling.					   */
    xscalespeed += DRANDOM(XSCALEFACTOR);
    yscalespeed += DRANDOM(YSCALEFACTOR);

    /* Check the scale rate agains the boundaries.			   */
    if (xscalespeed < XSCALESPEEDMIN) xscalespeed = XSCALESPEEDMIN;
    if (xscalespeed > XSCALESPEEDMAX) xscalespeed = XSCALESPEEDMAX;

    if (yscalespeed < YSCALESPEEDMIN) yscalespeed = YSCALESPEEDMIN;
    if (yscalespeed > YSCALESPEEDMAX) yscalespeed = YSCALESPEEDMAX;

    /* Check the scaling factor.					   */
    xscale += xscalespeed;
    yscale += yscalespeed;

    /* Work around what seemed like a rasteriation bug in 3.0. */
    while ((xscale > -SCALENOMANSLAND) && (xscale < SCALENOMANSLAND))
    	xscale += xscalespeed;
    while ((yscale > -SCALENOMANSLAND) && (yscale < SCALENOMANSLAND))
    	yscale += yscalespeed;

    /* Bounds check the scaling.					   */
    if (xscale < XSCALEMIN) {
	xscale = XSCALEMIN;
	xscalespeed = -xscalespeed;
      }
    if (xscale > XSCALEMAX) {
	xscale = XSCALEMAX;
	xscalespeed = -xscalespeed;
      }

    if (yscale < YSCALEMIN) {
	yscale = YSCALEMIN;
	yscalespeed = -yscalespeed;
      }
    if (yscale > YSCALEMAX) {
	yscale = YSCALEMAX;
	yscalespeed = -yscalespeed;
      }

    /* Calculate the new color bounce speeds.				   */
    rspeed += DRANDOM(COLORFACTOR);
    gspeed += DRANDOM(COLORFACTOR);
    bspeed += DRANDOM(COLORFACTOR);

    /* Bounds check the color speeds.					   */
    if (rspeed < COLORSPEEDMIN) rspeed = COLORSPEEDMIN;
    if (rspeed > COLORSPEEDMAX) rspeed = COLORSPEEDMAX;

    if (gspeed < COLORSPEEDMIN) gspeed = COLORSPEEDMIN;
    if (gspeed > COLORSPEEDMAX) gspeed = COLORSPEEDMAX;

    if (bspeed < COLORSPEEDMIN) bspeed = COLORSPEEDMIN;
    if (bspeed > COLORSPEEDMAX) bspeed = COLORSPEEDMAX;

    /* Change the colors by the speeds.					   */
    r += rspeed;
    g += gspeed;
    b += bspeed;

    /* Bounds check the color.						   */
    if (r < COLORMIN) {
	r = COLORMIN;
	rspeed = -rspeed;
      }
    if (r > COLORMAX) {
	r = COLORMAX;
	rspeed = -rspeed;
      }

    if (g < COLORMIN) {
	g = COLORMIN;
	gspeed = -gspeed;
      }
    if (g > COLORMAX) {
	g = COLORMAX;
	gspeed = -gspeed;
      }

    if (b < COLORMIN) {
	b = COLORMIN;
	bspeed = -bspeed;
      }
    if (b > COLORMAX) {
	b = COLORMAX;
	bspeed = -bspeed;
      }
      
    /* Transform the coordinate system and draw the text.		   */
    FFWTransform(x, y, xscale, yscale, rotation);
    PSsetrgbcolor(r, g, b);
    for ( whichLine = 0 ; whichLine < numLines ; whichLine++ ) {
      FFWText(startStrings[whichLine].x, startStrings[whichLine].y,
              displayStrings[whichLine]);
      }
    FFWDeTransform(-x, -y, 1.0/xscale, 1.0/yscale, -rotation);

    return self;
  }

- didLockFocus
  {
    [textFont set];
    return self;
  }

- initFrame:(NXRect *)frameRect
  {
    [super initFrame:frameRect];
    
    [inspectorPanel display];
    [self newSize];
    [self initValues];
     
    srandom(time(0));
    return self;
  }

-initValues
  {
    /* Initialize the position.						   */
    x = xmax / 2.0;
    y = ymax / 2.0;
    r = g = b = 0.5;
    rotation = 5.0;
    xscale = yscale = 1.0;

    /* Initialize the rate of change					   */
    xspeed = yspeed = 5.0;
    rotationspeed = 10.0;

    /* Initialize the overall control factor.				   */
    hyperness = 1.0;
      
    return self;
  }

- inspector:sender
{
    static NXDefaultsVector FlippyFloppyDefaults = {
    	{ "NXFont", "Times-Roman" },
	{ "Phrase", "Flippy\nFloppy" },
	{ NULL, NULL }};

    char buf[MAXPATHLEN];

    if (!inspectorPanel) {
      sprintf(buf,"%s/FlippyFloppy.nib",[sender
	      moduleDirectory:"FlippyFloppy"]);
      [NXApp loadNibFile:buf owner:self withNames:NO];

      /* Register our defaults. */
      NXRegisterDefaults("BackSpace.FlippyFloppy", FlippyFloppyDefaults);

      changed = YES;
      }
      
    return inspectorPanel;
}

  /* Attach ourselves to the end of a window's responder chain. */
- add:someone ToResponderListOf: whichWindow
  {
    id		prevResp, curResp;
    
    /* Put ourselves at the end of a window's responder chain. */
    prevResp = curResp = whichWindow;
    while ((curResp != someone) && (curResp != nil)) {
        /* Guarunteed to go through once. */
    	prevResp = curResp;
	curResp = [curResp nextResponder];
	}
    if (curResp == nil) [prevResp setNextResponder:someone];
    return self;
  }
  
  /* Remove ourselves from anywhere within a window's responder chain. */
- remove:someone FromResponderListOf: whichWindow
  {
    id		prevResp, curResp;

    /* Remove ourselve from the middle or end of a responder chain. */
    prevResp = curResp = whichWindow;
    while ((curResp != someone) && (curResp != nil)) {
        /* Guarunteed to go through once. */
    	prevResp = curResp;
	curResp = [curResp nextResponder];
	}
    if (curResp == someone) [prevResp setNextResponder:
                             [someone nextResponder]];
    return self;
  }
  

- inspectorInstalled
  {
    const char	*fontName, *phrase;

    /* Get ourselves access to the font panel. */    
    fontManager = [FontManager new];
    fontPanel = [fontManager getFontPanel:YES];
    
    /* We better not have a nextResponder before placing ourselves in
       the middle of these responder lists. */
    [self setNextResponder:nil];
    
    /* Get ourselves placed at the end of the responder chains for the
       windows we are dealing with. */
    [self add:self ToResponderListOf:fontPanel];
    
    /* Make the font panel a responder of the other two windows. */
    [self add:fontPanel ToResponderListOf:[inspectorPanel window]];
    [self add:fontPanel ToResponderListOf:[self window]];
    
    /* Make the FontManager use this module's action message. */
    [fontManager setAction:@selector(changeFontFlippyFloppy:)];

    /* Get the default font from the defaults database. */
    fontName = NXGetDefaultValue("BackSpace.FlippyFloppy", "NXFont");
    textFont = [Font newFont:fontName size:24 matrix:NX_IDENTITYMATRIX];
    [fontPanel setPanelFont:textFont isMultiple:NO];
    [fontNameDisplay setStringValue:[textFont displayName]];
    [flippyFloppyButton display];

    /* Get the default phrase from the database. */
    phrase = NXGetDefaultValue("BackSpace.FlippyFloppy", "Phrase");
    [textPanel setStringValue:phrase];

    changed = YES;
    
    return self;
  }
  
- inspectorWillBeRemoved
  {
    /* Hide the font panel. */
    [fontPanel orderWindow:NX_OUT relativeTo:0];     

    /* Make the FontManager use the generic action message. */
    [fontManager setAction:@selector(changeFont:)];

    /* Remove the fontpanel from the other window's responder lists.
       Remove ourselves from the fontPanel's responder list.  */
    [self remove:self FromResponderListOf:fontPanel];
    [self remove:fontPanel FromResponderListOf:[inspectorPanel window]];
    [self remove:fontPanel FromResponderListOf:[self window]];
        
    /* Restore the cache limit parameter for the context. */
    if (cacheLimit == 0) cacheLimit = 12500;
    FFWsetcachelimit(cacheLimit);

    return self;
  }

- sizeTo:(NXCoord)width :(NXCoord)height
  {
    [super sizeTo:width :height];
    [self newSize];
    return self;
  }

- newSize
  {
    xmax = bounds.size.width;
    ymax = bounds.size.height;
    return self;
  }

- drawSelf:(const NXRect *)rects :(int)rectCount
  {
    if (!rects || !rectCount)
      {
	return self;
      }
    PSsetgray(0.0);
    NXRectFill(rects);
    return self;
  }

- (BOOL)useBufferedWindow
  {
    return YES;
  }

- (const char *)windowTitle
  {
    return "Make FlippyFloppy!";
  }

- setHyperness: sender
  {
    hyperness = [sender floatValue];
    [hypernessField setFloatValue:hyperness];
    return self;
  }

- setFlippyText: sender
  {
   [textPanel selectText:self];
   NXWriteDefault("BackSpace.FlippyFloppy", "Phrase", [textPanel stringValue]);
   changed = YES;
   return self;
  }

- changeFontFlippyFloppy: sender
  {
   textFont = [sender convertFont:textFont];
   NXWriteDefault("BackSpace.FlippyFloppy", "NXFont", [textFont name]);
   [fontNameDisplay setStringValue:[textFont displayName]];
   [flippyFloppyButton display];
   changed = YES;
   return sender;
  }

- setFloppyFont: sender
  { 
    /* Make the FontManager use this module's action message. */
    [fontManager setAction:@selector(changeFontFlippyFloppy:)];
    
    /* Make the font panel appear! */
    [[fontPanel setPanelFont:textFont isMultiple:NO]
      makeKeyAndOrderFront:[inspectorPanel window]];     

    return self;
  }
  
- somethingChanged
  {
    const char	*textToSplit, *splitStart;
    int		whichLine, oldCacheLimit;
    TextBBox	linesBBox[MAX_TEXT_LINES];
    double	tweak, baseline;

    /* Zero out the cache limit - this is a *per context* variable (which is
       why we do it here - to make *sure* the context we are flipping text in
       has it temporarily zeroed out. */
    FFWzerocachelimit(&oldCacheLimit);
    if (oldCacheLimit != 0) cacheLimit = oldCacheLimit;

    /* Make sure we are using the right font. */
    [textFont set];
    [fontNameDisplay setStringValue:[textFont displayName]];
    [flippyFloppyButton display];
    
    /* Free the previous chunks of text, if any. */
    for ( whichLine = 0 ; whichLine < numLines ; whichLine++ ) {
      if (displayStrings[whichLine] != NULL) {
        (void)free(displayStrings[whichLine]);
	displayStrings[whichLine] = NULL;
	}
      }
      
    /* Figure out how many lines there are to display, keeping track of where
       each line begins.						   */
    whichLine = 0;
    textToSplit = [textPanel stringValue];
    if (textToSplit == NULL) return NULL;
    
    do {
      splitStart = textToSplit;
      while ((*textToSplit != 0) && (*textToSplit != '\n'))
        textToSplit++;

      /* Allocate the string chunk and copy the right part of the string. */
      displayStrings[whichLine] = malloc(textToSplit - splitStart + 1);
      if (displayStrings[whichLine] != NULL) {
        strncpy(displayStrings[whichLine], splitStart, 
	        textToSplit - splitStart);
        displayStrings[whichLine][textToSplit-splitStart] = 0;
	whichLine++;
	}
	
      /* Turn returns into string endings.				   */
      if (*textToSplit == '\n') textToSplit++;

    } while ((*textToSplit != 0) && (whichLine < MAX_TEXT_LINES));

    numLines = whichLine;

    /* Figure out the bounding boxes for each string, and at the same time
       what the total bounding box will be.				   */
    stringsBBox.llx = 100.0;
    stringsBBox.urx = -100.0;
    stringsBBox.lly = 0.0;
    stringsBBox.ury = 0.0;

    for (whichLine = 0 ; whichLine < numLines ; whichLine++) {
      FFWTextBBox(displayStrings[whichLine],
		  &linesBBox[whichLine].llx, &linesBBox[whichLine].lly,
		  &linesBBox[whichLine].urx, &linesBBox[whichLine].ury);

      /* The strings are centered over one another in the x direction.	   */
      if (linesBBox[whichLine].llx < stringsBBox.llx)
	stringsBBox.llx = linesBBox[whichLine].llx;
      if (linesBBox[whichLine].urx > stringsBBox.urx)
	stringsBBox.urx = linesBBox[whichLine].urx;

      /* The strings sit on top of one another in the y direction.	   */
      stringsBBox.lly += linesBBox[whichLine].lly;
      stringsBBox.ury += linesBBox[whichLine].ury + 2.0;
      }

    /* Now, tweak the bounding box to reflect the desire to have it centered
       around the origin.						   */
    tweak = (stringsBBox.llx + stringsBBox.urx) / 2.0;
    stringsBBox.llx -= tweak;
    stringsBBox.urx -= tweak;

    tweak = (stringsBBox.lly + stringsBBox.ury) / 2.0;
    stringsBBox.lly -= tweak;
    stringsBBox.ury -= tweak;

    /* Now, for each line of text, figure out where it's starting point is
       going to be.							   */
    baseline = stringsBBox.ury;
    for ( whichLine = 0 ; whichLine < numLines ; whichLine++ ) {

      tweak = (linesBBox[whichLine].llx + linesBBox[whichLine].urx) / 2.0;
      startStrings[whichLine].x = -tweak;

      baseline -= (linesBBox[whichLine].ury + 2.0);
      startStrings[whichLine].y = baseline;
      }

    /* Bump out the bounding box slightly. */
    stringsBBox.llx -= 6.0;	stringsBBox.lly -= 6.0;
    stringsBBox.urx += 6.0;	stringsBBox.ury += 6.0;

    /* Convert the bounding box into a rectangle.			   */
    stringsBBox.urx -= stringsBBox.llx;
    stringsBBox.ury -= stringsBBox.lly;

    return self;
  }
  
@end

